﻿using Silk.NET.OpenGL;
using Silk.NET.Windowing;
using StbImageSharp;
using System;
using System.Drawing;
using System.Runtime.CompilerServices;

namespace Meow.Core;

public enum MeshType
{
    Common,
    Instantiable,     // Instantiable rendered mesh with a fixed number of vertices
}

/// <summary>
/// RenderContext , currently only OpenGL
/// </summary>
public class Rd_Context
{
    public GL _gl;
    HashSet<Rd_VertexPrototype> VertexPrototypes = new HashSet<Rd_VertexPrototype>();
    Rd_VertexPrototype currentVertexPrototype;

    /// <summary>
    /// Set this will Call The Context to Bind VAO
    /// </summary>
    public Rd_VertexPrototype CurrentVertexPrototype
    {
        get { return currentVertexPrototype; }
        set {
            currentVertexPrototype = value;
            if (currentVertexPrototype != null)
                BindVAO(currentVertexPrototype.currentVAO);
            else
                BindVAO(0);
        }
    }
    public Rd_Context(IWindow appWindow)
    {
        _gl = appWindow.CreateOpenGL();
    }

    #region Abstract

    public void RegisterVertexPrototype(Rd_VertexPrototype vertexPrototype)
    {
        if (!VertexPrototypes.TryGetValue(vertexPrototype, out var actualValue))
        {
            CreateVAO(vertexPrototype, out var vaoID);
        }
    }

    /// <summary>
    /// Before use this CreateMesh Func, You should Set The CurrentVertexPrototype First.
    /// </summary>
    public Rd_MeshCommon CreateMesh(float[] vertices, uint[] indices, MeshType meshType)
    {
#if DEBUG
        if (CurrentVertexPrototype == null) throw new Exception("U should bind CurrentVertexPrototype before CreateMesh");
#endif

        if (meshType != MeshType.Common) throw new Exception("Only Common type can use now");

        uint buffer = 0;
        uint elembuffer = 0;

        CreateAndBindBuffer(BufferTargetARB.ArrayBuffer, ref buffer);
        CreateAndBindBuffer(BufferTargetARB.ElementArrayBuffer, ref elembuffer);

        unsafe
        {
            // 写入EBO
            fixed (uint* buf = indices)
                _gl.BufferData(BufferTargetARB.ElementArrayBuffer, (nuint)(indices.Length * sizeof(uint)), buf, BufferUsageARB.StaticDraw);

            // 解释VBO数据的结构,必须先绑定VBO
            int sizeCount = 0;
            uint totalSize = 0;
            for (uint i = 0; i < CurrentVertexPrototype.Def.Segments.Length; i++)
            {
                sizeCount += CurrentVertexPrototype.Def.Segments[i] * CurrentVertexPrototype.Def.Types[i].Size();
            }
            totalSize = (uint)sizeCount;
            sizeCount = 0;
            for (uint i = 0; i < CurrentVertexPrototype.Def.Segments.Length; i++)
            {
                _gl.EnableVertexAttribArray(i);
                _gl.VertexAttribPointer(i, CurrentVertexPrototype.Def.Segments[i], CurrentVertexPrototype.Def.Types[i], false, totalSize, (void*)sizeCount);
                sizeCount += CurrentVertexPrototype.Def.Segments[i] * CurrentVertexPrototype.Def.Types[i].Size();
            }

            // 写入VBO
            fixed (float* buf = vertices)
                _gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint)(vertices.Length * sizeof(float)), buf, BufferUsageARB.StaticDraw);
        }


        return new Rd_MeshCommon(CurrentVertexPrototype, buffer, elembuffer);
    }

    public void UnbindAllBuffer()
    {
        CurrentVertexPrototype = null;
        _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
        _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, 0);
    }

    /// <summary>
    /// Debug Only
    /// </summary>
    public unsafe void LoadTexture(string path, out uint textureId, bool flip = true)
    {
        if (!Path.Exists(path))
            throw new Exception($"LoadTexture : {path} does not exist");
        StbImage.stbi_set_flip_vertically_on_load(flip ? 1 : 0);
        ImageResult result = ImageResult.FromMemory(File.ReadAllBytes(path), ColorComponents.RedGreenBlueAlpha);

        textureId = _gl.GenTexture();
        _gl.ActiveTexture(TextureUnit.Texture0); // 绑定纹理前应该先Active一个纹理单元
        _gl.BindTexture(TextureTarget.Texture2D, textureId); // 将纹理绑定到当前Active的纹理单元

        _gl.TexParameterI(GLEnum.Texture2D, GLEnum.TextureWrapS, (int)TextureWrapMode.Repeat);
        _gl.TexParameterI(GLEnum.Texture2D, GLEnum.TextureWrapT, (int)TextureWrapMode.Repeat);
        _gl.TexParameterI(GLEnum.Texture2D, GLEnum.TextureMinFilter, (int)TextureMinFilter.Nearest);
        _gl.TexParameterI(GLEnum.Texture2D, GLEnum.TextureMagFilter, (int)TextureMagFilter.Nearest);

        fixed (byte* ptr = result.Data)
            // 这个操作会生成与当前绑定的纹理对象在同一个目标上的纹理，此处是TextureTarget.Texture2D
            _gl.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba, (uint)result.Width,
                (uint)result.Height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, ptr);

        _gl.GenerateMipmap(TextureTarget.Texture2D);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void BindTextureAt(int channel, uint textureId)
    {
        _gl.ActiveTexture(TextureUnit.Texture0 + channel);
        _gl.BindTexture(TextureTarget.Texture2D, textureId);
    }

    #endregion

    #region Buffer

    public unsafe void CreateVAO(Rd_VertexPrototype vertexPrototype, out uint vaoID)
    {
        vaoID = _gl.GenVertexArray();
        _gl.BindVertexArray(vaoID);

        //int sizeCount = 0;
        //uint totalSize = 0;
        //for (uint i = 0; i < vertexPrototype.Def.Segments.Length; i++)
        //{
        //    sizeCount += vertexPrototype.Def.Segments[i] * vertexPrototype.Def.Types[i].Size();
        //}
        //totalSize = (uint)sizeCount;
        //sizeCount = 0;
        //for (uint i = 0; i < vertexPrototype.Def.Segments.Length; i++)
        //{
        //    _gl.EnableVertexAttribArray(i);
        //    _gl.VertexAttribPointer(i, vertexPrototype.Def.Segments[i], vertexPrototype.Def.Types[i], false, totalSize, (void*)sizeCount);
        //    sizeCount += vertexPrototype.Def.Segments[i] * vertexPrototype.Def.Types[i].Size();
        //}

        vertexPrototype.currentContext = this;
        vertexPrototype.currentVAO = vaoID;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void BindVAO(uint vao) => _gl.BindVertexArray(vao);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void CreateAndBindBuffer(BufferTargetARB targetARB, ref uint bufferID)
    {
        bufferID = _gl.GenBuffer();
        _gl.BindBuffer(targetARB, bufferID);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void BindBuffer(BufferTargetARB targetARB, uint bufferID) => _gl.BindBuffer(targetARB, bufferID);

    #endregion

    #region Shader
    public bool CreateShader(string code, ShaderType shaderType, ref uint shaderID)
    {
        shaderID = _gl.CreateShader(shaderType);
        _gl.ShaderSource(shaderID, code);

        _gl.CompileShader(shaderID);
        _gl.GetShader(shaderID, ShaderParameterName.CompileStatus, out int vStatus);
        if (vStatus != (int)GLEnum.True)
        {
            Logger.Error(shaderType.ToString() + " Shader failed to compile: " + _gl.GetShaderInfoLog(shaderID));
            _gl.DeleteShader(shaderID);
            return false;
        }

        return true;
    }

    public bool CreateShaderProgram(uint shaderVert, uint shaderFrag, ref uint programID)
    {
        programID = _gl.CreateProgram();
        _gl.AttachShader(programID, shaderVert);
        _gl.AttachShader(programID, shaderFrag);
        _gl.LinkProgram(programID);

        _gl.GetProgram(programID, ProgramPropertyARB.LinkStatus, out int lStatus);
        if (lStatus != (int)GLEnum.True)
        {
            Logger.Error("Program failed to link: " + _gl.GetProgramInfoLog(programID));
            _gl.DetachShader(programID, shaderVert);
            _gl.DetachShader(programID, shaderFrag);
            _gl.DeleteProgram(programID);
            return false;
        }

        _gl.DetachShader(programID, shaderVert);
        _gl.DetachShader(programID, shaderFrag);
        return true;
    }



    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void DeleteShader(uint shaderID) => _gl.DeleteShader(shaderID);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void DeleteProgram(uint programID) => _gl.DeleteProgram(programID);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void UseProgram(uint program) => _gl.UseProgram(program);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public int GetUniformLocation(uint shaderProgram, string name) => _gl.GetUniformLocation(shaderProgram, name);

    #endregion

    #region Draw


    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Clear(ClearBufferMask mask) => _gl.Clear(mask);


    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void ClearColor(Color color) => _gl.ClearColor(color);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void ActiveTexture(TextureUnit texUnit) => _gl.ActiveTexture(texUnit);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void BindTexture(TextureTarget texTarget, uint texture) => _gl.BindTexture(texTarget, texture);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public unsafe void DrawElements(PrimitiveType mode, uint count, DrawElementsType drawElementsType, void* indices) => 
        _gl.DrawElements(mode, count, drawElementsType, indices);
    #endregion
}
